Published on

[Spring] Spring Transaction 에 대해 PART 2 - 옵션

Authors
  • avatar
    Name
    Woojin Son
    Twitter

Intro

지난 포스팅에서는 Spring Transactional 기능에 대한 개요에 대해 다뤄보았습니다. 그리고 추가로 Transactional 기능을 이해하기 위해 필요한 AOP 에 대한 포스팅도 추가로 다루었습니다.

이번 포스팅에서는 Spring Transactional 어노테이션 사용법에 대해 자세히 다뤄 보겠습니다.

복습

@Transactional 기능은 Spring AOP 를 통해 동작합니다. 프록시를 통해 해당 어노테이션이 걸린 컴포넌트를 인식하여 로직들에 대한 트랜잭션을 처리할 수있습니다.

AOP는 관점 지향 프로그래밍이었고, 핵심 관심사와 횡단 관심사를 분리합니다. @Transactional 과 같은 기능은 여러 로직들을 함께 어우르는 횡단 관심사라고 할 수 있습니다.

이 AOP를 구현하는 데 프록시가 사용되고, 프록시 인스턴스가 있기 때문에 원본 코드를 직접 수정 할 필요 없이 공통 기능들을 처리할 수 있습니다.

또한 트랜잭션은 아래와 같은 특징들을 가집니다.

  • 원자성 (Atomicity)
    • 한 트랜잭션 내의 실행 작업은 하나의 단위로 처리합니다.
  • 일관성 (Consistency)
    • 트랜잭션은 일관성 있는 데이터베이스 상태를 유지합니다.
  • 독립성 (Isolation)
    • 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않아야 합니다.
  • 지속성 (Durability)
    • 트랜잭션을 마무리하면 항상 결과를 저장시켜야 합니다.

@Transaction 세부 설정들 종류

크게 다섯가지를 설정할 수 있습니다.

  • Isolation (격리 수준 설정)
  • Propagation (전파 수준 설정)
  • readOnly
  • rollback / commit 예외설정
  • timeout

Isolation (격리 수준 설정)

어느 수준까지 일관성이 깨지는 것을 허용 할 것이냐에 대한 설정입니다.

@Transactional(isolation=Isolation.DEFAULT) @Transactional(isolation=Isolation.READ_COMMITTED)

우선 격리 수준에 대해 이해하기 전에 두 가지 용어를 설명하겠습니다.

  • Dirty Read
    • 특정 트랜잭션에 이해 데이터가 변경되었지만, 아직 커밋되지 않은 상황에서 다른 트랜잭션이 해당 변경 사항을 조회할 수 있는 문제를 말합니다.
  • Non-Repeatable Read
    • 같은 트랜잭션 내에서 같은 데이터를 여러번 조회했을 때 읽어온 데이터가 다른 경우를 의미합니다.
  • Phantom Read
    • Non-Repeatable Read의 한 종류로 조회해온 결과의 행이 새로 생기거나 없어지는 현상입니다.

Isonlation 옵션의 기본 값은 DEFAULT 이고 이 경우 DB의 규칙을 따릅니다. 하지만 스프링 단에서 권한이 생긴경우 아래의 네 가지 옵션을 줄 수 있습니다.

READ_UNCOMMITTED (Level 0)

아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 것을 허용합니다. 이 경우 Dirty Read 가 발생할 수 있습니다.

READ_COMMITTED (Level 1)

트랜잭션 처리 중 다른 트랜잭션에서 접근할 수 없도록 막습니다. 이 경우 Dirty Read 를 방지할 수 있고, 트랜잭션이 종료되기 전 까지는 해당 데이터에 접근할 수 없습니다.

REPEATABLE_READ (Level 2)

트랜잭션 처리 중 SELECT 문에서 사용하는 모든 데이터에 shared lock이 걸립니다. 이 경우 Non-Repeatable Read 방지 할 수 있지만, Phantom Read는 발생할 수 있습니다.

SERIALIZABLE (level 3)

가장 엄격한 격리 수준을 적용하여 완벽한 일관성을 제공합니다. Phantom Read 까지 방지할 수 있고 모든 격리 수준 관련 문제를 해결할 수 있지만, 성능 문제로 잘 사용하지 않습니다.

Propagation (전파 수준 설정)

@Transactional(Propagation=Propagation.REQUIRED)

Propagation 옵션은 로직 별로 트랜잭션은 전파 시킬 지 여부를 설정할 수 있습니다.

비즈니스 로직 전체가 하나의 트랜잭션이라고 보기는 어려울 수 있습니다. 트랜잭션의 단어 적 정의에 맞게 분리할 수 없는 작업의 단위로 나눈다면, 우리의 비즈니스 로직은 여러개의 트랜잭션으로 이루어 져 있다고도 볼 수 있으니까요.

그래서 상황에 맞춰서 트랜잭션의 전파 수준을 커스텀 해 줄 필요가 있습니다. A 작업이 실패하더라도 B작업은 반드시 실행시켜야 하는 경우가 생기기 때문입니다.

설정할 수 있는 속성들은 아래와 같습니다.

  • REQUIRED
  • REQUIRED_NEW
  • SUPPORTS
  • MANDATORY
  • NOT_SUPPORTED
  • NEVER
  • NESTED

설정 값들이 대해 설명하기 이전에 반드시 알아야 할 개념이 있습니다. 바로 물리 트랜잭션과 논리 트랜잭션입니다.

물리 트랜잭션은 데이터베이스의 트랜잭션을 사용한다는 의미입니다. 스프링 에서는 트랜잭션의 전파 속성에 따라서 동일한 물리 트랜잭션 내에 여러가지 트랜잭션들이 전파되거나 새로 생길 수 있습니다. 그러므로 Spring 에서는 논리 트랜잭션이라는 개념이 존재합니다.

  • 물리 트랜잭션: 실제 데이터베이스에 적용되는 트랜잭션으로, 커넥션을 통해 커밋/롤백하는 단위
  • 논리 트랜잭션: 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위

모든 논리 트랜잭션이 커밋 되어야 물리 트랜잭션이 커밋되고, 하나의 논리 트랜잭션이라도 롤백 된다면 물리 트랜잭션은 롤백 됩니다.

REQUIRED

Default 속성입니다. 트랜잭션이 필요하면 새로 만듭니다. 기존에 트랜잭션이 없다면 새로운 트랜잭션을 생성하고, 있다면 기존 트랜잭션에 참여합니다.

REQUIRED_NEW

물리 트랜잭션을 분리하는 옵션입니다. 이 경우 새로 트랜잭션을 전파시키는 게 아닙니다. 물리 트랜잭션이 분리 되어있기 때문에 논리 트랜잭션이 롤백 되더라도 상위 트랜잭션, 그니까 해당 트랜잭션을 호출 한 로직의 트랜잭션에는 영향이 가지 않습니다.

이 경우 DB 커넥션을 물리적으로 N개 나누는 것이기 때문에 잘못 사용하다간 DB 커넥션을 고갈시킬 수 있습니다.

기존 트랜잭션이 없다면 새로운 트랜잭션을 생성하고, 있다면 기존 트랜잭션을 보류시키고 새로운 트랜잭션을 생성합니다.

SUPPORTS

트랜잭션이 있으면 지원하고 없어도 상관없는 경우 사용합니다. 기존 트랜잭션이 없다면 따로 트랜잭션을 생성하지 않고, 있다면 참여합니다.

MANDATORY

트랜잭션이 반드시 필요한 경우 사용합니다. 기존 트랜잭션이 없다면 llegalTransactionStateException 예외를 발생시키고 있다면 참여합니다.

NOT_SUPPORTED

트랜잭션을 지원하지 않는 경우 사용합니다.. 기존 트랜잭션 없다면 트랜잭션 없이 진행하고 있다면 기존 트랜잭션을 보류시키고 트랜잭션 없이 진행합니다.

NEVER

트랜잭션을 사용하지 않는 경우, 그니까 기존 트랜잭션도 허용하지 않는 경우 사용합니다. 기존 트랜잭션이 없다면 트랜잭션 없이 진행하고 있다면 IllegalTransactionStateException 예외를 발생시킵니다.

NESTED

중첩 트랜잭션을 발생시켜야 하는 경우 사용합니다.

REQUIRED_NEW 와는 좀 다른데, 이 경우엔 기존 트랜잭션의 커밋과 롤백에 새로 생긴 트랜잭션이 영향을 받을 수 있습니다.

그리고 중첩 트랜잭션은 외부에 영향을 주지 않습니다. 롤백 된다고 해도 외부 트랜잭션은 커밋이 가능하고, 외부 트랜잭션이 롤백된다면 중첩 트랜잭션은 함께 롤백될 수 있습니다.

이 옵션은 JDBC의 savepoint 기능을 사용하므로 DB 드라이버의 지원여부를 체크해야하고 JPA에서 사용은 불가능합니다.

기존 트랜잭션이 없는 경우 새로운 트랜잭션을 생성하고 있다면 중첩 트랜잭션을 만듭니다.

Read Only

@Transactional(readOnly=true)

트랜잭션을 ReadOnly 로 사용하고 싶을 때 설정할 수 있습니다. 이 경우 스프링의 트랜잭션 기능을 제한하므로 성능 향상에 도움이 될 수 있습니다.

만약 DB를 이중화 해서 Read Only 가 존재하는 경우 해당 옵션을 통해 ReadOnly 데이터베이스에 쿼리를 전달하도록 처리할 수 있습니다.

class RoutingDatasource : AbstractRoutingDataSource {

    override fun determineCurrentLookupKey() : Unit {
        return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "readOnly" : "main";
    }
}

Read Only 옵션을 쓰는 경우 Optimistic Lock 의 동작에 영향을 끼칠 수 있습니다.

JPA에서는 동시성 제어를 위해 Optimistic(낙관적) / Pessimistic(비관적) Lock 을 사용합니다. 낙관적 락은 트랜잭션이 충돌하지 않을 것이라 가정하는 방법입니다. 트랜잭션이 데이터를 수정 할 때마다 버전 번호를 업데이트 하는 방식이고, 비관적 락은 DB의 락 기능을 사용합니다.

JPA를 쓰는 상황에서 만약 낙관적 락을 걸게 된다면 엔티티를 읽기 전용으로 읽었기 때문에 버전 번호를 확인하지 못할 수 있습니다.

Rollback

@Transactional(rollbackFor=GeneralException::class)
@Transactional(noRollbackFor=GeneralException::class)

특정 예외상황이 발생했을 때 롤백 여부를 결정하는 옵션입니다. Default 는 RuntimeException, Error 발생시 롤백 처리합니다.

Timeout

@Transactional(timeout=10)

일정 시간내에 트랜잭션이 완료되지 않은 경우 롤백처리 해야 할 때 사용합니다.

Outro

지금까지 @Transactional 어노테이션의 옵션들에 대해 알아보았습니다. 이번 포스팅을 쓰며 스프링에서 실제로 어떻게 트랜잭션을 관리하는 지 더욱 공부해볼 수 있었습니다. 읽어주셔서 감사합니다. 피드백은 언제든 환영입니다!